﻿// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

namespace Microsoft.Samples.BabelCam.Worker
{
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.ServiceModel;
    using System.Text;
    using System.Threading;
    using Microsoft.Samples.BabelCam.Infrastructure.Helpers;
    using Microsoft.Samples.BabelCam.Infrastructure.Models;
    using Microsoft.Samples.BabelCam.Worker.Helpers;
    using Microsoft.Samples.BabelCam.Worker.Models;
    using Microsoft.Samples.Hawaii.Services.Client;
    using Microsoft.Samples.Hawaii.Services.Client.Ocr;
    using Microsoft.WindowsAzure;
    using Microsoft.WindowsAzure.ServiceRuntime;
    using Microsoft.WindowsAzure.StorageClient;
    using WindowsPhone.Recipes.Push.Messages;

    public class WorkerRole : RoleEntryPoint
    {
        private string ocrServiceHostName;
        private string ocrServiceSignature;
        private string hawaiiApplicationId;
        private string translatorBaseAddress;
        private string translatorAppId;

        private string logContainerName;
        private string errorContainerName;

        private int batchSize;
        private int pollIntervalMiliseconds;

        private CloudStorageAccount account;
        private CloudBlobClient cloudBlobclient;
        private CloudQueue requestQueue;

        private ITranslatedWordsRepository translatedWordsRepository;
        private IPushUserEndpointsRepository pushUserEndpointsRepository;

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "This method initializes the Worker role.")]
        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // For information on handling configuration changes
            // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
            RoleEnvironment.Changing += this.RoleEnvironmentChanging;

            // This code sets up a handler to update CloudStorageAccount instances when their corresponding
            // configuration settings change in the service configuration file.
            CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
            {
                // Provide the configSetter with the initial value
                configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));

                RoleEnvironment.Changed += (sender, arg) =>
                {
                    if (arg.Changes.OfType<RoleEnvironmentConfigurationSettingChange>()
                        .Any((change) => (change.ConfigurationSettingName == configName)))
                    {
                        // The corresponding configuration setting has changed, propagate the value
                        if (!configSetter(RoleEnvironment.GetConfigurationSettingValue(configName)))
                        {
                            // In this case, the change to the storage account credentials in the
                            // service configuration is significant enough that the role needs to be
                            // recycled in order to use the latest settings. (for example, the 
                            // endpoint has changed)
                            RoleEnvironment.RequestRecycle();
                        }
                    }
                };
            });

            this.account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
            this.CreateCloudContainers(this.account, ConfigReader.GetConfigValue("RequestQueueName"), ConfigReader.GetConfigValue("ImageContainerName"));

            this.translatedWordsRepository = new TranslatedWordsServiceContext();
            this.pushUserEndpointsRepository = new UserTablesServiceContext();

            this.ocrServiceHostName = ConfigReader.GetConfigValue("OcrServiceHostName");
            this.ocrServiceSignature = ConfigReader.GetConfigValue("OcrServiceSignature");
            this.hawaiiApplicationId = ConfigReader.GetConfigValue("HawaiiApplicationId");
            this.translatorAppId = ConfigReader.GetConfigValue("TranslatorServiceAppId");
            this.translatorBaseAddress = ConfigReader.GetConfigValue("TranslatorServiceBaseAddress");

            this.logContainerName = ConfigReader.GetConfigValue("LogContainerName");
            this.errorContainerName = ConfigReader.GetConfigValue("ErrorContainerName");

            this.batchSize = int.Parse(ConfigReader.GetConfigValue("BatchSize"), CultureInfo.InvariantCulture);
            this.pollIntervalMiliseconds = int.Parse(ConfigReader.GetConfigValue("PollIntervalMiliseconds"), CultureInfo.InvariantCulture);

            return base.OnStart();
        }

        public override void Run()
        {
            // This is a sample worker implementation. Replace with your logic.
            Trace.WriteLine("BabelCam.Worker entry point called", "Information");

            while (true)
            {
                try
                {
                    foreach (var rawMessage in this.requestQueue.GetMessages(this.batchSize))
                    {
                        this.requestQueue.DeleteMessage(rawMessage);
                        var request = Extensions.Deserialize<OcrRequest>(rawMessage.AsString);

                        if (string.IsNullOrWhiteSpace(request.BlobName) || !request.BlobName.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
                        {
                            continue;
                        }

                        var container = this.cloudBlobclient.GetContainerReference(request.ContainerName);
                        var blob = container.GetBlobReference(request.BlobName);

                        try
                        {
                            ProcessOutput processOutput = null;
                            using (var stream = new MemoryStream())
                            {
                                blob.DownloadToStream(stream);

                                processOutput = this.ProcessImage(stream);
                            }

                            var words = string.Empty;
                            var wordCount = 0;
                            var status = string.Empty;
                            if (processOutput != null)
                            {
                                if (!processOutput.Attributes.TryGetValue("Status", out status))
                                {
                                    status = string.Empty;
                                }

                                if (!processOutput.Attributes.TryGetValue("Words", out words))
                                {
                                    words = string.Empty;
                                }

                                var wordCountString = string.Empty;
                                processOutput.Attributes.TryGetValue("WordCount", out wordCountString);
                                int.TryParse(wordCountString, out wordCount);
                            }

                            var message = string.Empty;
                            var translationRowKey = string.Empty;
                            if (status.Equals(Status.Success.ToString(), StringComparison.OrdinalIgnoreCase))
                            {
                                if (wordCount > 0)
                                {
                                    var translated = this.Translate(words, request.FromLanguage, request.ToLanguage);

                                    var wordEntity = new WordsEntity(translated, blob.Uri.ToString(), request.UserNameIdentifier);
                                    this.translatedWordsRepository.AddTranslatedWords(wordEntity);
                                    translationRowKey = wordEntity.RowKey;

                                    this.LogInformation(string.Concat("orig (", request.FromLanguage, ") for ", request.UserNameIdentifier, ": ", words));
                                    this.LogInformation(string.Concat("trans (", request.ToLanguage, ") for ", request.UserNameIdentifier, ": ", translated));

                                    message = translated;
                                }
                                else
                                {
                                    message = "[No text was found]";
                                    translationRowKey = string.Empty;
                                }
                            }
                            else
                            {
                                message = "[The OCR conversion failed]";
                                translationRowKey = string.Empty;
                            }

                            this.SendPushNotification(request.UserNameIdentifier, message, translationRowKey);
                        }
                        catch (Exception exception)
                        {
                            this.SendPushNotification(request.UserNameIdentifier, "There was an error processing your translation request.");
                            this.LogError(exception);
                        }
                    }
                }
                catch (Exception exception)
                {
                    this.LogError(exception);
                }

                Thread.Sleep(this.pollIntervalMiliseconds);
                Trace.WriteLine("Working", "Information");
            }
        }
        
        private void CreateCloudContainers(CloudStorageAccount account, string requestQueueName, string imageContainerName)
        {
            this.cloudBlobclient = account.CreateCloudBlobClient();
            var imageContainer = this.cloudBlobclient.GetContainerReference(imageContainerName);
            imageContainer.CreateIfNotExist();
            imageContainer.SetPermissions(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Blob });

            this.requestQueue = account.CreateCloudQueueClient().GetQueueReference(requestQueueName);
            this.requestQueue.CreateIfNotExist();

            CloudStorageInitializer.InitializeCloudStorage(account);
        }

        private OcrServiceResult ConvertToText(byte[] photoBuffer)
        {
            var ocrService = new OcrService(this.ocrServiceHostName, this.ocrServiceSignature);
            return (OcrServiceResult)ocrService.RecognizeImage(this.hawaiiApplicationId, photoBuffer);
        }

        private ProcessOutput ProcessImage(Stream imageStream)
        {
            var processOutput = new ProcessOutput();
            try
            {
                var words = string.Empty;
                byte[] buffer = new byte[imageStream.Length];

                imageStream.Seek(0, SeekOrigin.Begin);
                imageStream.Read(buffer, 0, buffer.Length);

                var result = this.ConvertToText(buffer);
                if ((result.Status == Status.Success) && (result.OcrResult.OcrTexts.Count > 0))
                {
                    var wordCount = 0;
                    var textSkew = 0;
                    var sb = new StringBuilder();
                    foreach (var item in result.OcrResult.OcrTexts)
                    {
                        wordCount += item.Words.Count;
                        sb.AppendLine(item.Text);
                        textSkew += int.Parse(item.Skew, System.Globalization.NumberStyles.Integer);
                    }

                    var avgTextSkew = textSkew / result.OcrResult.OcrTexts.Count;
                    if (wordCount != 0)
                    {
                        processOutput.Attributes["WordCount"] = wordCount.ToString();
                        processOutput.Attributes["Words"] = sb.ToString();
                        processOutput.Attributes["ConfidenceAvg"] = avgTextSkew.ToString();
                    }
                }

                processOutput.Attributes["Status"] = result.Status.ToString();
            }
            catch (Exception exception)
            {
                this.LogError(exception);
            }

            return processOutput;
        }

        private string Translate(string words, string fromLanguage, string toLanguage)
        {
            var translated = words;
            if (!fromLanguage.Equals(toLanguage, StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    using (var client = new TranslatorService.LanguageServiceClient(new BasicHttpBinding(), new EndpointAddress(this.translatorBaseAddress)))
                    {
                        var options = new TranslatorService.TranslateOptions();
                        var response = client.GetTranslations(this.translatorAppId, words, fromLanguage, toLanguage, 1, options);
                        translated = response.Translations[0].TranslatedText;
                    }
                }
                catch (Exception exception)
                {
                    this.LogError(exception);
                }
            }

            return translated;
        }

        private void SendPushNotification(string userName, string message, string translationRowKey = "")
        {
            var uris = this.pushUserEndpointsRepository.GetPushUsersByName(userName).Select(u => u.ChannelUri);
            var rawData = Encoding.UTF8.GetBytes(message);
            var rawMessage = new RawPushNotificationMessage
            {
                SendPriority = MessageSendPriority.High,
                RawData = (rawData.Length <= RawPushNotificationMessage.MaxPayloadSize)
                    ? rawData
                    : Encoding.UTF8.GetBytes(Extensions.Serialize<string>(translationRowKey))
            };

            foreach (var uri in uris)
            {
                try
                {
                    rawMessage.Send(new Uri(uri));
                }
                catch (Exception exception)
                {
                    this.LogError(exception);
                }
            }
        }

        private void LogInformation(string message)
        {
            var infoContainer = this.cloudBlobclient.GetContainerReference(this.logContainerName);
            infoContainer.CreateIfNotExist();

            var infoBlob = infoContainer.GetBlobReference((DateTime.MaxValue - DateTime.UtcNow).Ticks.ToString("d19") + ".txt");
            infoBlob.Properties.ContentType = "text/plain";
            infoBlob.UploadText(message);
        }

        private void LogError(Exception exception)
        {
            var errorsContainer = this.cloudBlobclient.GetContainerReference(this.errorContainerName);
            errorsContainer.CreateIfNotExist();

            var errorBlob = errorsContainer.GetBlobReference(string.Concat((DateTime.MaxValue - DateTime.UtcNow).Ticks.ToString("d19"), ".txt"));
            errorBlob.Properties.ContentType = "text/plain";
            errorBlob.UploadText(exception.ToString());
        }

        private void RoleEnvironmentChanging(object sender, RoleEnvironmentChangingEventArgs e)
        {
            // If a configuration setting is changing
            if (e.Changes.Any(change => change is RoleEnvironmentConfigurationSettingChange))
            {
                // Set e.Cancel to true to restart this role instance
                e.Cancel = true;
            }
        }
    }
}
